/*
 * Minecraft Forge
 * Copyright (c) 2016-2021.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation version 2.1
 * of the License.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

package net.minecraftforge.server.terminalconsole;

import javax.annotation.Nullable;

import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.apache.logging.log4j.core.pattern.ConverterKeys;
import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
import org.apache.logging.log4j.core.pattern.PatternConverter;
import org.apache.logging.log4j.core.pattern.PatternFormatter;
import org.apache.logging.log4j.core.pattern.PatternParser;
import org.apache.logging.log4j.util.PerformanceSensitive;
import org.apache.logging.log4j.util.PropertiesUtil;

import java.util.List;

/**
 * Replaces Minecraft formatting codes in the result of a pattern with
 * appropriate ANSI escape codes. The implementation will only replace valid
 * color codes using the section sign (§).
 *
 * <p>The {@link MinecraftFormattingConverter} can be only used together with
 * {@link TerminalConsoleAppender} to detect if the current console supports
 * color output. When running in an unsupported environment, it will
 * automatically strip all formatting codes instead.</p>
 *
 * <p>{@link TerminalConsoleAppender#ANSI_OVERRIDE_PROPERTY} may be used
 * to force the use of ANSI colors even in unsupported environments. As an
 * alternative, {@link #KEEP_FORMATTING_PROPERTY} may be used to keep the
 * raw Minecraft formatting codes.</p>
 *
 * <p><b>Example usage:</b> {@code %minecraftFormatting{%message}}<br>
 * It can be configured to always strip formatting codes from the message:
 * {@code %minecraftFormatting{%message}{strip}}</p>
 *
 * @see <a href="http://minecraft.gamepedia.com/Formatting_codes">
 *     Formatting Codes</a>
 */
@Plugin(name = "minecraftFormatting", category = PatternConverter.CATEGORY)
@ConverterKeys({ "minecraftFormatting" })
@PerformanceSensitive("allocation")
public class MinecraftFormattingConverter extends LogEventPatternConverter
{

	/**
	 * System property that allows disabling the replacement of Minecraft
	 * formatting codes entirely, keeping them in the console output. For
	 * some applications they might be easier and more accurate for parsing
	 * in applications like certain control panels.
	 *
	 * <p>If this system property is not set, or set to any value except
	 * {@code true}, all Minecraft formatting codes will be replaced
	 * or stripped from the console output.</p>
	 */
	public static final String KEEP_FORMATTING_PROPERTY = TerminalConsoleAppender.PROPERTY_PREFIX + ".keepMinecraftFormatting";

	private static final boolean KEEP_FORMATTING = PropertiesUtil.getProperties().getBooleanProperty(KEEP_FORMATTING_PROPERTY);

	private static final String ANSI_RESET = "\u001B[m";

	private static final char COLOR_CHAR = '\u00A7'; // §
	private static final String LOOKUP = "0123456789abcdefklmnor";

	private static final String[] ansiCodes = new String[] {
			"\u001B[0;30m", // Black §0
			"\u001B[0;34m", // Dark Blue §1
			"\u001B[0;32m", // Dark Green §2
			"\u001B[0;36m", // Dark Aqua §3
			"\u001B[0;31m", // Dark Red §4
			"\u001B[0;35m", // Dark Purple §5
			"\u001B[0;33m", // Gold §6
			"\u001B[0;37m", // Gray §7
			"\u001B[0;30;1m",  // Dark Gray §8
			"\u001B[0;34;1m",  // Blue §9
			"\u001B[0;32;1m",  // Green §a
			"\u001B[0;36;1m",  // Aqua §b
			"\u001B[0;31;1m",  // Red §c
			"\u001B[0;35;1m",  // Light Purple §d
			"\u001B[0;33;1m",  // Yellow §e
			"\u001B[0;37;1m",  // White §f
			"\u001B[5m",       // Obfuscated §k
			"\u001B[21m",      // Bold §l
			"\u001B[9m",       // Strikethrough §m
			"\u001B[4m",       // Underline §n
			"\u001B[3m",       // Italic §o
			ANSI_RESET,        // Reset §r
	};

	private final boolean ansi;
	private final List<PatternFormatter> formatters;

	/**
	 * Construct the converter.
	 *
	 * @param formatters The pattern formatters to generate the text to manipulate
	 * @param strip If true, the converter will strip all formatting codes
	 */
	protected MinecraftFormattingConverter(List<PatternFormatter> formatters, boolean strip)
	{
		super("minecraftFormatting", null);
		this.formatters = formatters;
		this.ansi = !strip;
	}

	@Override
	public void format(LogEvent event, StringBuilder toAppendTo)
	{
		int start = toAppendTo.length();
		//noinspection ForLoopReplaceableByForEach
		for (int i = 0, size = formatters.size(); i < size; i++)
		{
			formatters.get(i).format(event, toAppendTo);
		}

		if (KEEP_FORMATTING || toAppendTo.length() == start)
		{
			// Skip replacement if disabled or if the content is empty
			return;
		}

		String content = toAppendTo.substring(start);
		format(content, toAppendTo, start, ansi && TerminalConsoleAppender.isAnsiSupported());
	}

	private static void format(String s, StringBuilder result, int start, boolean ansi)
	{
		int next = s.indexOf(COLOR_CHAR);
		int last = s.length() - 1;
		if (next == -1 || next == last)
		{
			return;
		}

		result.setLength(start + next);

		int pos = next;
		do
		{
			int format = LOOKUP.indexOf(Character.toLowerCase(s.charAt(next + 1)));
			if (format != -1)
			{
				if (pos != next)
				{
					result.append(s, pos, next);
				}
				if (ansi)
				{
					result.append(ansiCodes[format]);
				}
				pos = next += 2;
			}
			else
			{
				next++;
			}

			next = s.indexOf(COLOR_CHAR, next);
		} while (next != -1 && next < last);

		result.append(s, pos, s.length());
		if (ansi)
		{
			result.append(ANSI_RESET);
		}
	}

	/**
	 * Gets a new instance of the {@link MinecraftFormattingConverter} with the
	 * specified options.
	 *
	 * @param config The current configuration
	 * @param options The pattern options
	 * @return The new instance
	 *
	 * @see MinecraftFormattingConverter
	 */
	@Nullable
	public static MinecraftFormattingConverter newInstance(Configuration config, String[] options)
	{
		if (options.length < 1 || options.length > 2)
		{
			LOGGER.error("Incorrect number of options on minecraftFormatting. Expected at least 1, max 2 received " + options.length);
			return null;
		}
		if (options[0] == null)
		{
			LOGGER.error("No pattern supplied on minecraftFormatting");
			return null;
		}

		PatternParser parser = PatternLayout.createPatternParser(config);
		List<PatternFormatter> formatters = parser.parse(options[0]);
		boolean strip = options.length > 1 && "strip".equals(options[1]);
		return new MinecraftFormattingConverter(formatters, strip);
	}
}
